package cz.pproproj.aaservice.guimodule.config;

import cz.pproproj.aaservice.guimodule.BLLModuleAccess;
import cz.pproproj.aaservice.guimodule.model.AuthenticationException;
import cz.pproproj.aaservice.guimodule.model.User;
import cz.pproproj.aaservice.guimodule.auth.TokenInfo;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.stereotype.Component;
import cz.pproproj.aaservice.guimodule.model.Credentials;

import java.util.HashMap;
import java.util.Map;

/**
 * Service that performs authentication and authorization of credentials.
 *
 * @author Zdenek Obst, z.obst-at-gmc.net
 */
@Component
public class AuthService {

    private static final Map<User, TokenInfo> TOKENS = new HashMap<User, TokenInfo>(); // so far in-memory
    private static final Map<User, Integer> REFRESHES_COUNTER = new HashMap<User,Integer>();
    private static final int ACCESS_TOKEN_LENGTH = 19;
    private static final int REFRESH_TOKEN_LENGTH = 17;
    /**
     * How long the token is valid until it expires.
     */
    private static final long TOKENS_EXPIRATION = 24 * 60 * 60 * 1000; // 1 day
    /**
     * How many times client can refresh token before new authentication (against LDAP) is required again.
     */
    private static final int MAX_TOKEN_REFRESHES = 5;

    private final BLLModuleAccess _bllModuleAccess = new AnnotationConfigApplicationContext("cz.pproproj.aaservice.aasbllmodule")
            .getBean(BLLModuleAccess.class);


    public UserTokens authenticate(Credentials credentials, String moduleName) throws AuthenticationException {
        if (credentials == null) {
            throw new AuthenticationException("Missing credentials");
        }
        if (credentials.isTokenBased()) {
            String accessToken = credentials.getToken();
            validateLength(accessToken, ACCESS_TOKEN_LENGTH);
            UserTokens userTokens = lookupTokens(credentials.getToken(), true);
            checkExpiration(userTokens.getTokenInfo());

            return userTokens;
        } else if (!moduleName.isEmpty()){
            User user = _bllModuleAccess.authenticate(credentials, moduleName);

            TokenInfo tokenInfo = generateTokens(moduleName);
            TOKENS.put(user, tokenInfo);
            REFRESHES_COUNTER.put(user, 0);
            return new UserTokens(user, tokenInfo);
        }else{
            throw new AuthenticationException("No Authentication was selected");
        }
    }


    public void invalidateTokens(Credentials credentials) throws Exception{
        UserTokens tokens = authenticate(credentials, null);
        User user = tokens.getUser();
        TOKENS.remove(user);
        REFRESHES_COUNTER.remove(user);
    }



    public UserTokens refresh(String refreshToken) throws Exception {
        validateLength(refreshToken, REFRESH_TOKEN_LENGTH);
        UserTokens userInfo = lookupTokens(refreshToken, false);
        incrementRefreshes(userInfo.getUser());
        TokenInfo newTokens = generateTokens(userInfo.getTokenInfo().getModule());
        TOKENS.put(userInfo.getUser(), newTokens);
        return new UserTokens(userInfo.getUser(), newTokens);
    }




    private void checkExpiration(TokenInfo tokenInfo) throws AuthenticationException {
        long now = System.currentTimeMillis();
        if (now > tokenInfo.getExpires()) {
            throw new AuthenticationException("Token has expired");
        }
    }


    private UserTokens lookupTokens(String token, boolean isAccess) throws AuthenticationException {

        for (Map.Entry<User, TokenInfo> entry : TOKENS.entrySet()) {
            TokenInfo tokenInfo = entry.getValue();
            String tokenToCompare = isAccess ? tokenInfo.getAccessToken() : tokenInfo.getRefreshToken();
            if (tokenToCompare.equals(token)) {
                User user = entry.getKey();
                return new UserTokens(user, tokenInfo);
            }
        }

        throw new AuthenticationException();
    }

    private void incrementRefreshes(User user) throws Exception {
        Integer refreshes = REFRESHES_COUNTER.get(user);
        if (refreshes == null) {
            refreshes = 0;
        }
        refreshes++;
        REFRESHES_COUNTER.put(user, refreshes);
            if (refreshes > MAX_TOKEN_REFRESHES) {
            throw new Exception("You already refreshed your token " + MAX_TOKEN_REFRESHES + " times," +
                    " you must authenticate again with your LDAP credentials.");
        }
    }

    private void validateLength(String token, int expectedLength) throws AuthenticationException {
        if (token == null || token.length() != expectedLength) {
            throw new AuthenticationException();
        }
    }

    private TokenInfo generateTokens(String moduleName) {
        String accessToken = StringUtil.randomAlphanumeric(ACCESS_TOKEN_LENGTH);
        String refreshToken = StringUtil.randomAlphanumeric(REFRESH_TOKEN_LENGTH);
        long expires = System.currentTimeMillis() + TOKENS_EXPIRATION;
        return new TokenInfo(accessToken, refreshToken, expires, moduleName);
    }


    public static class UserTokens {

        private final User user;
        private final TokenInfo tokenInfo;

        public UserTokens(User user, TokenInfo tokenInfo) {
            this.user = user;
            this.tokenInfo = tokenInfo;
        }

        public User getUser() {
            return user;
        }

        public TokenInfo getTokenInfo() {
            return tokenInfo;
        }
    }


}
